using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Phoenix.Diagnostics;
using Phoenix.Frames;
using Phoenix.Lifetime;

namespace Phoenix.Framework
{
    public class ActionInvoker : IActionInvoker
    {
        readonly Dictionary<IContentFrame, Queue<KeyValuePair<InvokeActionRequest, Action<ControllerActionContext>>>> requestQueue;
        readonly Dictionary<IContentFrame, ActionExecution> currentRequests;
        readonly object invokeLock = new object();
        readonly IErrorReporter errorReporter;
        readonly Func<IPhoenixHost> hostFactory;
        IPhoenixHost phoenixHost;
        readonly IControllerFactory controllerFactory;

        public ActionInvoker(IErrorReporter errorReporter, Func<IPhoenixHost> hostFactory, IControllerFactory controllerFactory)
        {
            requestQueue = new Dictionary<IContentFrame, Queue<KeyValuePair<InvokeActionRequest, Action<ControllerActionContext>>>>();
            currentRequests = new Dictionary<IContentFrame, ActionExecution>();
            this.errorReporter = errorReporter;
            this.hostFactory = hostFactory;
            this.controllerFactory = controllerFactory;
        }

        public IPhoenixHost Host
        {
            get { return phoenixHost ?? (phoenixHost = hostFactory()); }
        }

        internal Dictionary<IContentFrame, Queue<KeyValuePair<InvokeActionRequest, Action<ControllerActionContext>>>> RequestQueue
        {
            get { return requestQueue; }
        }

        public Task InvokeAction(InvokeActionRequest request)
        {
            return InvokeInternal(request, c => { });
        }

        public Task<TResult> InvokeActionWithResult<TResult>(InvokeActionRequest request)
        {
            var internalCompletionSource = new TaskCompletionSource<object>();
            InvokeInternal(request, c =>
            {
                c.CompletionSource = internalCompletionSource;
            })
            .ContinueWith(t=>
            {
                if (t.IsCanceled)
                {
                    internalCompletionSource.SetCanceled();
                    return;
                }
                if (t.Exception != null)
                {
                    internalCompletionSource.SetException(t.Exception.Flatten().InnerExceptions);
                    return;
                }
            });

            var tcs = new TaskCompletionSource<TResult>();

            // We need to have a TaskCompletionSource<object> to pass around, 
            // but we need a TaskCompletionSource<TResult> to return
            // this code maps the two together
            internalCompletionSource
                .Task.ContinueWith(t =>
                    {
                        if (t.IsCanceled)
                        {
                            tcs.SetCanceled();
                            return;
                        }
                        if (t.Exception != null)
                        {
                            tcs.SetException(t.Exception.Flatten().InnerExceptions);
                            return;
                        }

                        tcs.SetResult((TResult)t.Result);
                    });

            return tcs.Task;
        }

        internal Task InvokeInternal(InvokeActionRequest invokeActionRequest, Action<ControllerActionContext> contextSetup)
        {
            ValidateRequest(invokeActionRequest);

            lock (invokeLock)
            {
                Task currentExecutingTask;
                if (!IsBackground(invokeActionRequest) && QueueIfFrameIsBusy(invokeActionRequest, contextSetup, out currentExecutingTask))
                    return currentExecutingTask;

                return InvokeInternalAsync(invokeActionRequest, contextSetup);
            }
        }

        static bool IsBackground(InvokeActionRequest invokeActionRequest)
        {
            return (invokeActionRequest.Options & InvokeOptions.InBackground) == InvokeOptions.InBackground;
        }

        Task InvokeInternalAsync(InvokeActionRequest invokeActionRequest, Action<ControllerActionContext> contextSetup)
        {
            var newContext = CreateControllerActionContext(invokeActionRequest, contextSetup);
            var actionExecution = new ActionExecution(invokeActionRequest, newContext, errorReporter, Host,
                Host.ControllerLocator, controllerFactory);
            actionExecution.Execute(phoenixHost.AppDispatcher, phoenixHost.AppScheduler);

            if (!IsBackground(invokeActionRequest))
                currentRequests.Add(invokeActionRequest.TargetFrame, actionExecution);

            return actionExecution
                .ContinueWith(t=>
                    {
                        if (!IsBackground(invokeActionRequest))
                            currentRequests.Remove(invokeActionRequest.TargetFrame);
                        RunNextEnqueuedTask(invokeActionRequest.TargetFrame);
                        t.Wait(); // Propogate exception
                    }, TaskScheduler.Default); //We block on this, we need to make sure we are never on the UI thread
        }

        static void ValidateRequest(InvokeActionRequest invokeActionRequest)
        {
            if (invokeActionRequest.TargetFrame == null)
                invokeActionRequest.TargetFrame = invokeActionRequest.CurrentContext.NavigationFrame;

            if (invokeActionRequest.TargetFrame == null)
                throw new InvalidOperationException("When current context is empty, you must specify a target frame");
        }

        ControllerActionContext CreateControllerActionContext(InvokeActionRequest invokeActionRequest, Action<ControllerActionContext> contextSetup)
        {
            var navigationFrame = invokeActionRequest.TargetFrame as INavigationFrame ?? invokeActionRequest.CurrentContext.NavigationFrame;
            var newContext = new ControllerActionContext(invokeActionRequest.Target, navigationFrame, invokeActionRequest.TargetFrame, Host,
                                                         invokeActionRequest.RequestTransientStore ?? new RequestTransientStore(),
                                                         invokeActionRequest.Parameters ?? Enumerable.Empty<ControllerActionParameter>(),
                                                         invokeActionRequest.Options);

            if (invokeActionRequest.CurrentContext != null &&
                invokeActionRequest.CurrentContext.CompletionSource != null)
            {
                newContext.CompletionSource = invokeActionRequest.CurrentContext.CompletionSource;
            }

            contextSetup(newContext);
            return newContext;
        }

        bool QueueIfFrameIsBusy(InvokeActionRequest invokeActionRequest, Action<ControllerActionContext> contextSetup, out Task currentTask)
        {
            if (currentRequests.ContainsKey(invokeActionRequest.TargetFrame))
            {
                var currentRequest = currentRequests[invokeActionRequest.TargetFrame];

                if (!RequestQueue.ContainsKey(invokeActionRequest.TargetFrame))
                {
                    var queue = new Queue<KeyValuePair<InvokeActionRequest, Action<ControllerActionContext>>>();
                    RequestQueue.Add(invokeActionRequest.TargetFrame, queue);
                }
                var keyValuePair = new KeyValuePair<InvokeActionRequest, Action<ControllerActionContext>>(invokeActionRequest, contextSetup);
                RequestQueue[invokeActionRequest.TargetFrame].Enqueue(keyValuePair);
                Trace.WriteInfo(this, () => string.Format("Enqueued {0}", invokeActionRequest.Target));
                {
                    currentTask = currentRequest.ExecutionTask;
                    return true;
                }
            }

            currentTask = null;
            return false;
        }

        void RunNextEnqueuedTask(IContentFrame targetFrame)
        {
            if (RequestQueue.Count > 0 && RequestQueue.ContainsKey(targetFrame) && RequestQueue[targetFrame].Count > 0)
            {
                var queuedRequest = RequestQueue[targetFrame].Dequeue();
                InvokeInternal(queuedRequest.Key, queuedRequest.Value).Wait();
            }
        }
    }
}